Skip to content

feature/capr 30 create global error handler - #67

Merged
shamikkarkhanis merged 13 commits into
developfrom
feature/capr-30-create-global-error-handler
Feb 6, 2026
Merged

feature/capr 30 create global error handler#67
shamikkarkhanis merged 13 commits into
developfrom
feature/capr-30-create-global-error-handler

Conversation

@shamikkarkhanis

@shamikkarkhanis shamikkarkhanis commented Feb 5, 2026

Copy link
Copy Markdown
Member
  • docs: update agents
  • feat(error): global error handling applied everywhere

Summary by Sourcery

Introduce centralized, user-friendly error handling for both slash and prefix commands and add supporting utilities, tests, and documentation updates.

New Features:

  • Add a global error handler for slash commands that surfaces user-friendly messages and logs unexpected errors.
  • Add a global error handler for prefix commands that surfaces user-friendly messages and logs unexpected errors.
  • Introduce a CapyError/UserFriendlyError exception hierarchy for representing user-displayable errors.
  • Add an error-test cog to deliberately trigger generic and user-friendly errors for verification.

Enhancements:

  • Update UI views and utility embeds to use a standardized error_embed helper with sensible defaults for error responses.
  • Deprecate direct use of the global capy_discord.instance in favor of dependency injection, retaining a backward-compatible access shim with warnings.

Build:

  • Add pytest-asyncio as a development dependency for async test support.

Documentation:

  • Document the deprecation of the global capy_discord.instance in the cog standards guide.

Tests:

  • Add tests covering global slash and prefix error handling for both user-friendly and generic errors, including logger selection and interaction response paths.
  • Add tests for the error-test cog to validate error-raising behavior for both slash and prefix commands.
  • Add tests ensuring errors in ping and sync commands bubble to the global handler instead of being locally caught.
  • Add tests for the error_embed helper and custom error classes to verify defaults, inheritance, and attributes.

dependabot Bot and others added 9 commits February 4, 2026 12:26
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.14 to 0.15.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.14.14...0.15.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.2 to 7.13.3.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](coveragepy/coveragepy@7.13.2...7.13.3)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.13.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [uv](https://github.com/astral-sh/uv) from 0.9.28 to 0.9.30.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](astral-sh/uv@0.9.28...0.9.30)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.9.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.14 to 0.0.15.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ty@0.0.14...0.0.15)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
build(deps-dev): bump ty from 0.0.14 to 0.0.15
build(deps-dev): bump uv from 0.9.28 to 0.9.30
build(deps-dev): bump coverage from 7.13.2 to 7.13.3
build(deps-dev): bump ruff from 0.14.14 to 0.15.0
@sourcery-ai

sourcery-ai Bot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduce centralized global error handling for both slash and prefix commands using a new error hierarchy and standardized error embeds, deprecate the global bot instance access pattern in favor of dependency injection, and simplify individual commands so errors bubble into the new handlers, with tests covering the new behavior.

Sequence diagram for global slash command error handling

sequenceDiagram
    actor User
    participant DiscordAPI
    participant Bot
    participant SlashCommand as SlashCommandHandler
    participant ErrorHandler as Bot_on_tree_error
    participant Embeds as error_embed

    User->>DiscordAPI: Invoke slash command
    DiscordAPI->>Bot: InteractionCreate
    Bot->>SlashCommand: Execute command
    SlashCommand-->>Bot: app_commands.AppCommandError
    Bot->>ErrorHandler: on_tree_error(interaction, error)

    alt Error is CommandInvokeError wrapping UserFriendlyError
        ErrorHandler->>ErrorHandler: Unpack CommandInvokeError
        ErrorHandler->>Embeds: error_embed(description=actual_error.user_message)
        Embeds-->>ErrorHandler: error_embed_instance
        alt interaction.response.is_done is True
            ErrorHandler->>DiscordAPI: interaction.followup.send(embed, ephemeral=True)
        else interaction.response.is_done is False
            ErrorHandler->>DiscordAPI: interaction.response.send_message(embed, ephemeral=True)
        end
    else Error is generic
        ErrorHandler->>ErrorHandler: Unpack CommandInvokeError if needed
        ErrorHandler->>Bot: _get_logger_for_command(interaction.command)
        Bot-->>ErrorHandler: logger_for_module
        ErrorHandler->>ErrorHandler: logger.exception("Slash command error")
        ErrorHandler->>Embeds: error_embed(description="An unexpected error occurred. Please try again later.")
        Embeds-->>ErrorHandler: error_embed_instance
        alt interaction.response.is_done is True
            ErrorHandler->>DiscordAPI: interaction.followup.send(embed, ephemeral=True)
        else interaction.response.is_done is False
            ErrorHandler->>DiscordAPI: interaction.response.send_message(embed, ephemeral=True)
        end
    end
Loading

Sequence diagram for global prefix command error handling

sequenceDiagram
    actor User
    participant DiscordAPI
    participant Bot
    participant PrefixCommand as PrefixCommandHandler
    participant ErrorHandler as Bot_on_command_error
    participant Embeds as error_embed

    User->>DiscordAPI: Invoke prefix command
    DiscordAPI->>Bot: MessageCreate
    Bot->>PrefixCommand: Execute command
    PrefixCommand-->>Bot: commands.CommandError
    Bot->>ErrorHandler: on_command_error(ctx, error)

    alt Error is CommandInvokeError wrapping UserFriendlyError
        ErrorHandler->>ErrorHandler: Unpack CommandInvokeError
        ErrorHandler->>Embeds: error_embed(description=actual_error.user_message)
        Embeds-->>ErrorHandler: error_embed_instance
        ErrorHandler->>DiscordAPI: ctx.send(embed)
    else Error is generic
        ErrorHandler->>ErrorHandler: Unpack CommandInvokeError if needed
        ErrorHandler->>Bot: _get_logger_for_command(ctx.command)
        Bot-->>ErrorHandler: logger_for_module
        ErrorHandler->>ErrorHandler: logger.exception("Command error")
        ErrorHandler->>Embeds: error_embed(description="An unexpected error occurred. Please try again later.")
        Embeds-->>ErrorHandler: error_embed_instance
        ErrorHandler->>DiscordAPI: ctx.send(embed)
    end
Loading

ER diagram for error entities and their usage

erDiagram
    BOT ||--o{ SLASH_COMMAND : executes
    BOT ||--o{ PREFIX_COMMAND : executes

    ERROR_BASE ||--|{ USER_FRIENDLY_ERROR : subtype

    BOT }o--|| ERROR_BASE : logs
    BOT }o--|| USER_FRIENDLY_ERROR : maps_to_embed

    VIEW_BASE }o--|| ERROR_BASE : logs

    EMBED_UTILITY ||--o{ ERROR_EMBED : produces

    BOT }o--|| ERROR_EMBED : sends
    VIEW_BASE }o--|| ERROR_EMBED : sends

    BOT {
        string name
    }

    SLASH_COMMAND {
        string name
    }

    PREFIX_COMMAND {
        string name
    }

    ERROR_BASE {
        string message
    }

    USER_FRIENDLY_ERROR {
        string message
        string user_message
    }

    VIEW_BASE {
        string identifier
    }

    EMBED_UTILITY {
        string default_error_title
    }

    ERROR_EMBED {
        string title
        string description
    }
Loading

Class diagram for Bot error pipeline and error types

classDiagram
    class Bot {
        +log logging.Logger
        +setup_hook() async
        +_get_logger_for_command(command) logging.Logger
        +on_tree_error(interaction, error) async
        +on_command_error(ctx, error) async
        +load_extensions() async
    }

    class commands_AutoShardedBot {
    }

    Bot --|> commands_AutoShardedBot

    class CapyError {
        <<exception>>
    }

    class UserFriendlyError {
        <<exception>>
        +user_message str
        +UserFriendlyError(message str, user_message str)
    }

    UserFriendlyError --|> CapyError

    class error_embed_function {
        +error_embed(title str = "❌ Error", description str = "") discord.Embed
    }

    class BaseView {
        +log logging.Logger
        +on_error(interaction, error, item) async
        +on_timeout() async
        +disable_all_items() void
        +reply(interaction, content str, embed discord.Embed, embeds list~discord.Embed~, file discord.File, files list~discord.File~, view discord.ui.View, ephemeral bool, delete_after float, allowed_mentions discord.AllowedMentions, attachments list~discord.Attachment~, suppress_embeds bool) async
    }

    class ui_View {
    }

    BaseView --|> ui_View

    Bot ..> UserFriendlyError : handles
    Bot ..> error_embed_function : uses
    BaseView ..> error_embed_function : uses
Loading

Class diagram for deprecated global instance access

classDiagram
    class capy_discord_module {
        -_instance Bot | None
        +__getattr__(name str) object
    }

    class Bot {
    }

    capy_discord_module o--> Bot : _instance

    class main_module {
        +main() void
    }

    main_module ..> capy_discord_module : sets instance (deprecated)

    note for capy_discord_module "Accessing instance via attribute triggers DeprecationWarning; use dependency injection instead"
Loading

File-Level Changes

Change Details Files
Add centralized, user-friendly error handling to the Bot for slash and prefix commands using a shared error hierarchy and standardized embeds.
  • Extend Bot to subclass commands.AutoShardedBot and wire tree.on_error to a new on_tree_error handler.
  • Implement _get_logger_for_command to route logs to the module-specific logger when available, falling back to the bot logger.
  • Implement on_tree_error to unwrap CommandInvokeError, handle UserFriendlyError with a user-facing error_embed, and log & respond with a generic error embed otherwise, handling both initial and followup responses.
  • Implement on_command_error to similarly unwrap CommandInvokeError, handle UserFriendlyError with a user-facing error_embed, and log & respond with a generic error embed, using module-specific logging when possible.
capy_discord/bot.py
Introduce a dedicated error type hierarchy and testing utilities for user-friendly and generic error flows.
  • Add CapyError base exception and UserFriendlyError with a separate user_message attribute for safe user display.
  • Add an internal ErrorTest cog exposing slash and prefix commands that intentionally raise generic and UserFriendlyError exceptions for exercising the error pipeline.
  • Add unit tests covering the CapyError/UserFriendlyError behavior and the ErrorTest cog flows, including direct callback invocation.
capy_discord/errors.py
capy_discord/exts/tools/_error_test.py
tests/capy_discord/test_errors.py
tests/capy_discord/exts/test_error_test_cog.py
Standardize error presentation via an error_embed helper and integrate it into interactive UI views and handlers.
  • Change error_embed to provide sensible defaults for title and description while preserving the red error color.
  • Update BaseView.on_error to log errors and respond with a standardized error_embed rather than a raw string, using followup vs response based on interaction state.
  • Tighten BaseView.disable_all_items typing for children and default the reply embed parameter to discord.utils.MISSING for consistency with discord.py APIs.
  • Add unit tests verifying error_embed defaults and custom title behavior.
capy_discord/ui/embeds.py
capy_discord/ui/views.py
tests/capy_discord/test_error_utility.py
Simplify existing commands so errors propagate to the global handlers instead of being caught locally, and ensure this behavior via tests.
  • Remove local try/except and ad-hoc error messages from the ping slash command, leaving logging and interaction response only on the success path.
  • Remove local try/except from prefix and slash sync commands so that sync failures bubble to the global error handlers, retaining logging and normal description formatting.
  • Add tests for ping success and error propagation, and for sync commands to assert that errors from bot.tree.sync bubble up as exceptions.
  • Ensure sync_slash still defers responses ephemerally before performing sync operations.
capy_discord/exts/tools/ping.py
capy_discord/exts/tools/sync.py
tests/capy_discord/exts/test_ping.py
tests/capy_discord/exts/test_sync.py
Deprecate the global capy_discord.instance pattern while keeping a backwards-compatible shim, and document the new recommendation.
  • Replace the simple instance attribute with a private _instance and a getattr implementation that exposes instance while emitting a DeprecationWarning and enforcing attribute access for other names.
  • Annotate the deprecated global bot instance in main as deprecated and recommend dependency injection instead, while still assigning the Bot instance for now.
  • Update AGENTS contributor documentation to mark capy_discord.instance as deprecated and reiterate that all cogs must accept bot in init.
capy_discord/__init__.py
capy_discord/__main__.py
AGENTS.md
Add async-testing support and comprehensive tests for the new error-handling behaviors across the bot.
  • Add pytest-asyncio as a development dependency to support testing of async bot and cog methods.
  • Add tests that exercise Bot.on_tree_error and Bot.on_command_error for both UserFriendlyError and generic exceptions, including cases where interaction responses are already done and module-specific loggers are used.
  • Add fallback-logger tests ensuring that when a command/module is missing, the bot logger is used for error logging.
pyproject.toml
tests/capy_discord/test_error_handling.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@shamikkarkhanis
shamikkarkhanis changed the base branch from main to develop February 5, 2026 21:15

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues, and left some high level feedback:

  • The new capy_discord.instance deprecation shim looks incorrect: assigning capy_discord.instance = Bot(...) in __main__ creates a real module attribute and bypasses __getattr__, so the deprecation warning never fires and _instance is never updated—if you want to keep compatibility while warning, consider keeping a real instance attribute and emitting the warning from a helper or at assignment time instead of via __getattr__.
  • The _error_test.ErrorTest cog’s raised exceptions (ValueError("Generic error"), UserFriendlyError("Log", "User message")) don’t match the messages used in the tests and description (e.g. tests expect "Generic Test Error" / "Internal Error Log" / "User Message"), which will cause brittle or failing assertions—align the exception messages (and types if needed) with the intended test expectations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `capy_discord.instance` deprecation shim looks incorrect: assigning `capy_discord.instance = Bot(...)` in `__main__` creates a real module attribute and bypasses `__getattr__`, so the deprecation warning never fires and `_instance` is never updated—if you want to keep compatibility while warning, consider keeping a real `instance` attribute and emitting the warning from a helper or at assignment time instead of via `__getattr__`.
- The `_error_test.ErrorTest` cog’s raised exceptions (`ValueError("Generic error")`, `UserFriendlyError("Log", "User message")`) don’t match the messages used in the tests and description (e.g. tests expect "Generic Test Error" / "Internal Error Log" / "User Message"), which will cause brittle or failing assertions—align the exception messages (and types if needed) with the intended test expectations.

## Individual Comments

### Comment 1
<location> `capy_discord/__init__.py:14-23` </location>
<code_context>
+_instance: Bot | None = None
+
+
+def __getattr__(name: str) -> object:
+    if name == "instance":
+        warnings.warn(
+            "capy_discord.instance is deprecated. Use dependency injection.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return _instance
+
+    msg = f"module {__name__!r} has no attribute {name!r}"
+    raise AttributeError(msg)
</code_context>

<issue_to_address>
**issue (bug_risk):** The deprecation mechanism for `capy_discord.instance` is bypassed by normal attribute assignment and `_instance` is never updated.

Because `__main__.py` assigns `capy_discord.instance = Bot(...)`, it creates a real module attribute and `__getattr__` is no longer called for `instance`. As a result, the deprecation warning only fires before the first assignment (if ever), and `_instance` is never updated, so it cannot be treated as the canonical store.

To align behavior with the intended deprecation path, you could either:
- Add a `__setattr__` (or dedicated setter) that updates `_instance` and emits the warning on write, or
- Make `instance: Bot | None = None` a normal module attribute and emit the warning through property-like accessors, avoiding `_instance` entirely.
</issue_to_address>

### Comment 2
<location> `capy_discord/ui/views.py:60` </location>
<code_context>
         interaction: discord.Interaction,
         content: str | None = None,
-        embed: discord.Embed | None = None,
+        embed: discord.Embed = discord.utils.MISSING,
         embeds: list[discord.Embed] = discord.utils.MISSING,
         file: discord.File = discord.utils.MISSING,
</code_context>

<issue_to_address>
**suggestion:** The `embed` parameter type no longer reflects the actual values (including `MISSING` and `None`) that can be passed through.

With the new default, `embed` can now be `discord.Embed`, `discord.utils.MISSING`, or `None` (if explicitly passed), but the annotation only reflects `discord.Embed`. Static type checkers will assume `embed` is never `None`/`MISSING`. Consider updating the annotation to something like `discord.Embed | None | discord.utils.MissingType` (or a local alias) so it matches the actual possible values.

Suggested implementation:

```python
        interaction: discord.Interaction,
        content: str | None = None,
        embed: discord.Embed | None | discord.utils.MissingType = discord.utils.MISSING,
        embeds: list[discord.Embed] = discord.utils.MISSING,
        file: discord.File = discord.utils.MISSING,

```

If you prefer not to reference `discord.utils.MissingType` inline, you could:
1. Import `MissingType` (e.g. `from discord.utils import MissingType`), and
2. Change the annotation to `discord.Embed | None | MissingType`.
Also consider applying the same pattern to other parameters that use `discord.utils.MISSING` as a default (e.g. `embeds`, `file`, `files`) so their type hints match their actual possible values.
</issue_to_address>

### Comment 3
<location> `capy_discord/bot.py:41-43` </location>
<code_context>
+                await interaction.response.send_message(embed=embed, ephemeral=True)
+            return
+
+        # Generic error handling
+        logger = self._get_logger_for_command(interaction.command)
+        logger.exception("Slash command error: %s", error)
+        embed = error_embed(description="An unexpected error occurred. Please try again later.")
+        if interaction.response.is_done():
</code_context>

<issue_to_address>
**suggestion:** The logged error message uses the wrapper exception instead of the unwrapped `actual_error`, which may reduce log clarity.

In `on_tree_error` and `on_command_error` you unpack `CommandInvokeError` into `actual_error`, but still log `error`. Please switch the format argument to `actual_error` so the underlying exception type/message is visible, e.g.:
```python
logger.exception("Slash command error: %s", actual_error)
```
The same applies to the prefix command handler.

Suggested implementation:

```python
        # Generic error handling
        logger = self._get_logger_for_command(interaction.command)
        logger.exception("Slash command error: %s", actual_error)
        embed = error_embed(description="An unexpected error occurred. Please try again later.")

```

You should also update the prefix command handler (`on_command_error`) in the same way. Wherever you have a pattern roughly like:

```python
actual_error = error.original
logger.exception("Command error: %s", error)
```

change it to:

```python
actual_error = error.original
logger.exception("Command error: %s", actual_error)
```

so that the underlying exception type and message are logged consistently for both slash and prefix commands.
</issue_to_address>

### Comment 4
<location> `tests/capy_discord/exts/test_error_test_cog.py:22-24` </location>
<code_context>
+
+
+@pytest.mark.asyncio
+async def test_error_test_generic(cog):
+    interaction = MagicMock(spec=discord.Interaction)
+    with pytest.raises(ValueError, match="Generic Test Error"):
+        await cog.error_test(interaction, "generic")
+
</code_context>

<issue_to_address>
**issue (testing):** The expectations in these tests don’t match the current ErrorTest cog messages and will fail as written.

`ErrorTest` currently raises `ValueError("Generic error")` and `UserFriendlyError("Log", "User message")`, but these tests expect `"Generic Test Error"` and `"Internal Error Log"`. As written, they will always fail. Please either update the `match=` patterns (and any related expectations) to reflect the current messages, or change the implementation if the test expectations represent the intended contract.
</issue_to_address>

### Comment 5
<location> `tests/capy_discord/exts/test_sync.py:24-31` </location>
<code_context>
+
+
+@pytest.mark.asyncio
+async def test_sync_command_error_bubbles(cog, bot):
+    ctx = MagicMock(spec=commands.Context)
+    ctx.bot = bot
+    ctx.author.id = 123
+    ctx.send = AsyncMock()
+    bot.tree.sync.side_effect = Exception("Sync failed")
+
+    with pytest.raises(Exception, match="Sync failed"):
+        await cog.sync.callback(cog, ctx)
+
</code_context>

<issue_to_address>
**suggestion (testing):** Sync tests only cover error bubbling; consider adding success and edge-case scenarios.

These tests validate exception bubbling from `tree.sync`, but don’t exercise normal behavior of the refactored commands. Please also add:

- A happy-path test for `sync` to assert the expected description text, successful `_sync_commands` behavior, and logging.
- A happy-path test for `sync_slash` to assert `defer` is called, the followup message lists the correct commands, and logging occurs.
- `spec` edge-case tests:
  - `spec` in `{'.', 'guild'}` with `ctx.guild` set, asserting guild-specific sync and messaging.
  - `spec` in `{'.', 'guild'}` with `ctx.guild` is `None`, asserting the early return and the "must be used in a guild" message.
  - `spec = 'clear'` to cover the clear-commands branch.

This will better ensure the error-handling refactor hasn’t regressed normal sync behavior.

Suggested implementation:

```python
@pytest.fixture
def cog(bot):
    return Sync(bot)


@pytest.mark.asyncio
async def test_sync_command_error_bubbles(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.send = AsyncMock()
    bot.tree.sync.side_effect = Exception("Sync failed")

    with pytest.raises(Exception, match="Sync failed"):
        await cog.sync.callback(cog, ctx)


@pytest.mark.asyncio
async def test_sync_command_success(cog, bot, caplog):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    # Make sure sync succeeds
    bot.tree.sync = AsyncMock(return_value=["cmd1", "cmd2"])

    with caplog.at_level("INFO"):
        await cog.sync.callback(cog, ctx)

    bot.tree.sync.assert_awaited_once()
    ctx.send.assert_awaited_once()
    # Relaxed assertion: just ensure we sent some success text
    sent_args, sent_kwargs = ctx.send.await_args
    assert "sync" in sent_args[0].lower()

    # Ensure we logged something about sync succeeding
    assert any("sync" in r.getMessage().lower() for r in caplog.records)


@pytest.mark.asyncio
async def test_sync_slash_success(cog, bot, caplog):
    interaction = MagicMock(spec=discord.Interaction)
    interaction.client = bot
    interaction.guild = None
    interaction.user.id = 123
    interaction.response.defer = AsyncMock()
    interaction.followup.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["cmd1", "cmd2"])

    with caplog.at_level("INFO"):
        await cog.sync_slash.callback(cog, interaction)

    interaction.response.defer.assert_awaited_once()
    bot.tree.sync.assert_awaited_once()

    interaction.followup.send.assert_awaited_once()
    args, kwargs = interaction.followup.send.await_args
    assert "cmd1" in args[0]
    assert "cmd2" in args[0]

    assert any("sync" in r.getMessage().lower() for r in caplog.records)


@pytest.mark.asyncio
async def test_sync_spec_guild_with_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = MagicMock()
    ctx.guild.id = 456
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])
    await cog.sync.callback(cog, ctx, spec="guild")

    bot.tree.sync.assert_awaited_once()
    # Expect a guild-specific sync (implementation may use guild / guild_id)
    call_kwargs = bot.tree.sync.await_args.kwargs
    assert "guild" in call_kwargs or "guild_id" in call_kwargs

    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_guild_without_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])

    await cog.sync.callback(cog, ctx, spec="guild")

    # Should early-return without calling sync
    bot.tree.sync.assert_not_awaited()
    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "must be used in a guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_dot_with_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = MagicMock()
    ctx.guild.id = 456
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])
    await cog.sync.callback(cog, ctx, spec=".")

    bot.tree.sync.assert_awaited_once()
    call_kwargs = bot.tree.sync.await_args.kwargs
    assert "guild" in call_kwargs or "guild_id" in call_kwargs

    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_dot_without_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])

    await cog.sync.callback(cog, ctx, spec=".")

    bot.tree.sync.assert_not_awaited()
    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "must be used in a guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_clear(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    bot.tree.clear_commands = MagicMock()
    bot.tree.sync = AsyncMock(return_value=[])

    await cog.sync.callback(cog, ctx, spec="clear")

    bot.tree.clear_commands.assert_called_once()
    bot.tree.sync.assert_awaited_once()
    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "cleared" in sent_args[0].lower()

```

1. Ensure the top of `tests/capy_discord/exts/test_sync.py` imports the testing utilities used in these tests:
   ```python
   from unittest.mock import AsyncMock, MagicMock
   import pytest
   import discord
   from discord.ext import commands

   from capy_discord.exts.tools.sync import Sync
   ```
   (If these are already present, avoid duplicating them.)
2. The tests assume:
   - `Sync.sync` is a regular command whose underlying callback can be invoked as `cog.sync.callback(cog, ctx, spec=None)`.
   - `Sync.sync_slash` is an application command whose callback can be invoked as `cog.sync_slash.callback(cog, interaction, spec=None)`.
   - For guild-specific sync (`spec in {'.', 'guild'}`), the implementation passes a `guild` or `guild_id` keyword to `bot.tree.sync`.
   - When a guild-only spec is used without a guild, the implementation sends a message containing "must be used in a guild".
   - For `spec="clear"`, the implementation calls `bot.tree.clear_commands()` and then `bot.tree.sync()`, and sends a message containing "cleared".
   If the actual implementation differs, adjust the assertions (especially message text and `bot.tree.sync` call-shape) to match the real behavior.
</issue_to_address>

### Comment 6
<location> `tests/capy_discord/test_error_utility.py:6-13` </location>
<code_context>
+from capy_discord.ui.embeds import error_embed
+
+
+def test_error_embed_defaults():
+    """Test error_embed with default values."""
+    description = "Something went wrong"
+    embed = error_embed(description=description)
+
+    assert embed.title == "❌ Error"
+    assert embed.description == description
+    assert embed.color == discord.Color.red()
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Nice coverage for error_embed; consider complementary tests for view-level error handling that consumes it.

Since `BaseView.on_error` now uses `error_embed` and branches on `interaction.response.is_done()`, it would be useful to add view-level tests that:

- Mock a `discord.Interaction` where `response.is_done()` returns both `False` and `True` to verify it calls `response.send_message` vs `followup.send` as expected.
- Assert that `on_error` uses `error_embed` for the embed and that the message is ephemeral.

That will complement these unit tests by covering the end-to-end error handling behavior on views.

Suggested implementation:

```python
import discord
from unittest.mock import AsyncMock, MagicMock

import pytest

from capy_discord.ui.embeds import error_embed
from capy_discord.ui.view import BaseView

```

```python
    embed = error_embed(description=description)

    assert embed.title == "❌ Error"
    assert embed.description == description
    assert embed.color == discord.Color.red()


@pytest.mark.asyncio
async def test_base_view_on_error_uses_error_embed_response_send_message(monkeypatch):
    """BaseView.on_error should use error_embed and response.send_message when response is not done."""
    # Arrange
    sentinel_embed = discord.Embed(title="sentinel")

    def fake_error_embed(*args, **kwargs):
        return sentinel_embed

    # Patch where BaseView uses error_embed, not the test module import
    monkeypatch.setattr("capy_discord.ui.view.error_embed", fake_error_embed)

    interaction = MagicMock(spec=discord.Interaction)

    interaction.response = AsyncMock()
    interaction.response.is_done.return_value = False
    interaction.response.send_message = AsyncMock()

    interaction.followup = AsyncMock()
    interaction.followup.send = AsyncMock()

    view = BaseView()

    # Act
    await view.on_error(RuntimeError("boom"), item=None, interaction=interaction)

    # Assert
    interaction.response.send_message.assert_awaited_once_with(
        embed=sentinel_embed,
        ephemeral=True,
    )
    interaction.followup.send.assert_not_awaited()


@pytest.mark.asyncio
async def test_base_view_on_error_uses_error_embed_followup_send(monkeypatch):
    """BaseView.on_error should use error_embed and followup.send when response is already done."""
    # Arrange
    sentinel_embed = discord.Embed(title="sentinel")

    def fake_error_embed(*args, **kwargs):
        return sentinel_embed

    monkeypatch.setattr("capy_discord.ui.view.error_embed", fake_error_embed)

    interaction = MagicMock(spec=discord.Interaction)

    interaction.response = AsyncMock()
    interaction.response.is_done.return_value = True
    interaction.response.send_message = AsyncMock()

    interaction.followup = AsyncMock()
    interaction.followup.send = AsyncMock()

    view = BaseView()

    # Act
    await view.on_error(RuntimeError("boom"), item=None, interaction=interaction)

    # Assert
    interaction.followup.send.assert_awaited_once_with(
        embed=sentinel_embed,
        ephemeral=True,
    )
    interaction.response.send_message.assert_not_awaited()

```

1. If `BaseView` lives in a different module than `capy_discord.ui.view`, update the import and both `monkeypatch.setattr` targets accordingly.
2. If your test suite uses a different async test marker (e.g. `pytest.mark.anyio`), adjust the `@pytest.mark.asyncio` decorators to match your configuration.
3. If `BaseView.on_error` has a different signature or additional required parameters, update the `await view.on_error(...)` calls to match that signature.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread capy_discord/__init__.py
Comment thread capy_discord/ui/views.py Outdated
interaction: discord.Interaction,
content: str | None = None,
embed: discord.Embed | None = None,
embed: discord.Embed = discord.utils.MISSING,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The embed parameter type no longer reflects the actual values (including MISSING and None) that can be passed through.

With the new default, embed can now be discord.Embed, discord.utils.MISSING, or None (if explicitly passed), but the annotation only reflects discord.Embed. Static type checkers will assume embed is never None/MISSING. Consider updating the annotation to something like discord.Embed | None | discord.utils.MissingType (or a local alias) so it matches the actual possible values.

Suggested implementation:

        interaction: discord.Interaction,
        content: str | None = None,
        embed: discord.Embed | None | discord.utils.MissingType = discord.utils.MISSING,
        embeds: list[discord.Embed] = discord.utils.MISSING,
        file: discord.File = discord.utils.MISSING,

If you prefer not to reference discord.utils.MissingType inline, you could:

  1. Import MissingType (e.g. from discord.utils import MissingType), and
  2. Change the annotation to discord.Embed | None | MissingType.
    Also consider applying the same pattern to other parameters that use discord.utils.MISSING as a default (e.g. embeds, file, files) so their type hints match their actual possible values.

Comment thread capy_discord/bot.py
Comment on lines +41 to +43
# Generic error handling
logger = self._get_logger_for_command(interaction.command)
logger.exception("Slash command error: %s", error)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The logged error message uses the wrapper exception instead of the unwrapped actual_error, which may reduce log clarity.

In on_tree_error and on_command_error you unpack CommandInvokeError into actual_error, but still log error. Please switch the format argument to actual_error so the underlying exception type/message is visible, e.g.:

logger.exception("Slash command error: %s", actual_error)

The same applies to the prefix command handler.

Suggested implementation:

        # Generic error handling
        logger = self._get_logger_for_command(interaction.command)
        logger.exception("Slash command error: %s", actual_error)
        embed = error_embed(description="An unexpected error occurred. Please try again later.")

You should also update the prefix command handler (on_command_error) in the same way. Wherever you have a pattern roughly like:

actual_error = error.original
logger.exception("Command error: %s", error)

change it to:

actual_error = error.original
logger.exception("Command error: %s", actual_error)

so that the underlying exception type and message are logged consistently for both slash and prefix commands.

Comment on lines +22 to +24
async def test_error_test_generic(cog):
interaction = MagicMock(spec=discord.Interaction)
with pytest.raises(ValueError, match="Generic Test Error"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): The expectations in these tests don’t match the current ErrorTest cog messages and will fail as written.

ErrorTest currently raises ValueError("Generic error") and UserFriendlyError("Log", "User message"), but these tests expect "Generic Test Error" and "Internal Error Log". As written, they will always fail. Please either update the match= patterns (and any related expectations) to reflect the current messages, or change the implementation if the test expectations represent the intended contract.

Comment on lines +24 to +31
async def test_sync_command_error_bubbles(cog, bot):
ctx = MagicMock(spec=commands.Context)
ctx.bot = bot
ctx.author.id = 123
ctx.send = AsyncMock()
bot.tree.sync.side_effect = Exception("Sync failed")

with pytest.raises(Exception, match="Sync failed"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Sync tests only cover error bubbling; consider adding success and edge-case scenarios.

These tests validate exception bubbling from tree.sync, but don’t exercise normal behavior of the refactored commands. Please also add:

  • A happy-path test for sync to assert the expected description text, successful _sync_commands behavior, and logging.
  • A happy-path test for sync_slash to assert defer is called, the followup message lists the correct commands, and logging occurs.
  • spec edge-case tests:
    • spec in {'.', 'guild'} with ctx.guild set, asserting guild-specific sync and messaging.
    • spec in {'.', 'guild'} with ctx.guild is None, asserting the early return and the "must be used in a guild" message.
    • spec = 'clear' to cover the clear-commands branch.

This will better ensure the error-handling refactor hasn’t regressed normal sync behavior.

Suggested implementation:

@pytest.fixture
def cog(bot):
    return Sync(bot)


@pytest.mark.asyncio
async def test_sync_command_error_bubbles(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.send = AsyncMock()
    bot.tree.sync.side_effect = Exception("Sync failed")

    with pytest.raises(Exception, match="Sync failed"):
        await cog.sync.callback(cog, ctx)


@pytest.mark.asyncio
async def test_sync_command_success(cog, bot, caplog):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    # Make sure sync succeeds
    bot.tree.sync = AsyncMock(return_value=["cmd1", "cmd2"])

    with caplog.at_level("INFO"):
        await cog.sync.callback(cog, ctx)

    bot.tree.sync.assert_awaited_once()
    ctx.send.assert_awaited_once()
    # Relaxed assertion: just ensure we sent some success text
    sent_args, sent_kwargs = ctx.send.await_args
    assert "sync" in sent_args[0].lower()

    # Ensure we logged something about sync succeeding
    assert any("sync" in r.getMessage().lower() for r in caplog.records)


@pytest.mark.asyncio
async def test_sync_slash_success(cog, bot, caplog):
    interaction = MagicMock(spec=discord.Interaction)
    interaction.client = bot
    interaction.guild = None
    interaction.user.id = 123
    interaction.response.defer = AsyncMock()
    interaction.followup.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["cmd1", "cmd2"])

    with caplog.at_level("INFO"):
        await cog.sync_slash.callback(cog, interaction)

    interaction.response.defer.assert_awaited_once()
    bot.tree.sync.assert_awaited_once()

    interaction.followup.send.assert_awaited_once()
    args, kwargs = interaction.followup.send.await_args
    assert "cmd1" in args[0]
    assert "cmd2" in args[0]

    assert any("sync" in r.getMessage().lower() for r in caplog.records)


@pytest.mark.asyncio
async def test_sync_spec_guild_with_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = MagicMock()
    ctx.guild.id = 456
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])
    await cog.sync.callback(cog, ctx, spec="guild")

    bot.tree.sync.assert_awaited_once()
    # Expect a guild-specific sync (implementation may use guild / guild_id)
    call_kwargs = bot.tree.sync.await_args.kwargs
    assert "guild" in call_kwargs or "guild_id" in call_kwargs

    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_guild_without_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])

    await cog.sync.callback(cog, ctx, spec="guild")

    # Should early-return without calling sync
    bot.tree.sync.assert_not_awaited()
    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "must be used in a guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_dot_with_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = MagicMock()
    ctx.guild.id = 456
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])
    await cog.sync.callback(cog, ctx, spec=".")

    bot.tree.sync.assert_awaited_once()
    call_kwargs = bot.tree.sync.await_args.kwargs
    assert "guild" in call_kwargs or "guild_id" in call_kwargs

    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_dot_without_guild(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    bot.tree.sync = AsyncMock(return_value=["gcmd"])

    await cog.sync.callback(cog, ctx, spec=".")

    bot.tree.sync.assert_not_awaited()
    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "must be used in a guild" in sent_args[0].lower()


@pytest.mark.asyncio
async def test_sync_spec_clear(cog, bot):
    ctx = MagicMock(spec=commands.Context)
    ctx.bot = bot
    ctx.author.id = 123
    ctx.guild = None
    ctx.send = AsyncMock()

    bot.tree.clear_commands = MagicMock()
    bot.tree.sync = AsyncMock(return_value=[])

    await cog.sync.callback(cog, ctx, spec="clear")

    bot.tree.clear_commands.assert_called_once()
    bot.tree.sync.assert_awaited_once()
    ctx.send.assert_awaited_once()
    sent_args, _ = ctx.send.await_args
    assert "cleared" in sent_args[0].lower()
  1. Ensure the top of tests/capy_discord/exts/test_sync.py imports the testing utilities used in these tests:
    from unittest.mock import AsyncMock, MagicMock
    import pytest
    import discord
    from discord.ext import commands
    
    from capy_discord.exts.tools.sync import Sync
    (If these are already present, avoid duplicating them.)
  2. The tests assume:
    • Sync.sync is a regular command whose underlying callback can be invoked as cog.sync.callback(cog, ctx, spec=None).
    • Sync.sync_slash is an application command whose callback can be invoked as cog.sync_slash.callback(cog, interaction, spec=None).
    • For guild-specific sync (spec in {'.', 'guild'}), the implementation passes a guild or guild_id keyword to bot.tree.sync.
    • When a guild-only spec is used without a guild, the implementation sends a message containing "must be used in a guild".
    • For spec="clear", the implementation calls bot.tree.clear_commands() and then bot.tree.sync(), and sends a message containing "cleared".
      If the actual implementation differs, adjust the assertions (especially message text and bot.tree.sync call-shape) to match the real behavior.

Comment on lines +6 to +13
def test_error_embed_defaults():
"""Test error_embed with default values."""
description = "Something went wrong"
embed = error_embed(description=description)

assert embed.title == "❌ Error"
assert embed.description == description
assert embed.color == discord.Color.red()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Nice coverage for error_embed; consider complementary tests for view-level error handling that consumes it.

Since BaseView.on_error now uses error_embed and branches on interaction.response.is_done(), it would be useful to add view-level tests that:

  • Mock a discord.Interaction where response.is_done() returns both False and True to verify it calls response.send_message vs followup.send as expected.
  • Assert that on_error uses error_embed for the embed and that the message is ephemeral.

That will complement these unit tests by covering the end-to-end error handling behavior on views.

Suggested implementation:

import discord
from unittest.mock import AsyncMock, MagicMock

import pytest

from capy_discord.ui.embeds import error_embed
from capy_discord.ui.view import BaseView
    embed = error_embed(description=description)

    assert embed.title == "❌ Error"
    assert embed.description == description
    assert embed.color == discord.Color.red()


@pytest.mark.asyncio
async def test_base_view_on_error_uses_error_embed_response_send_message(monkeypatch):
    """BaseView.on_error should use error_embed and response.send_message when response is not done."""
    # Arrange
    sentinel_embed = discord.Embed(title="sentinel")

    def fake_error_embed(*args, **kwargs):
        return sentinel_embed

    # Patch where BaseView uses error_embed, not the test module import
    monkeypatch.setattr("capy_discord.ui.view.error_embed", fake_error_embed)

    interaction = MagicMock(spec=discord.Interaction)

    interaction.response = AsyncMock()
    interaction.response.is_done.return_value = False
    interaction.response.send_message = AsyncMock()

    interaction.followup = AsyncMock()
    interaction.followup.send = AsyncMock()

    view = BaseView()

    # Act
    await view.on_error(RuntimeError("boom"), item=None, interaction=interaction)

    # Assert
    interaction.response.send_message.assert_awaited_once_with(
        embed=sentinel_embed,
        ephemeral=True,
    )
    interaction.followup.send.assert_not_awaited()


@pytest.mark.asyncio
async def test_base_view_on_error_uses_error_embed_followup_send(monkeypatch):
    """BaseView.on_error should use error_embed and followup.send when response is already done."""
    # Arrange
    sentinel_embed = discord.Embed(title="sentinel")

    def fake_error_embed(*args, **kwargs):
        return sentinel_embed

    monkeypatch.setattr("capy_discord.ui.view.error_embed", fake_error_embed)

    interaction = MagicMock(spec=discord.Interaction)

    interaction.response = AsyncMock()
    interaction.response.is_done.return_value = True
    interaction.response.send_message = AsyncMock()

    interaction.followup = AsyncMock()
    interaction.followup.send = AsyncMock()

    view = BaseView()

    # Act
    await view.on_error(RuntimeError("boom"), item=None, interaction=interaction)

    # Assert
    interaction.followup.send.assert_awaited_once_with(
        embed=sentinel_embed,
        ephemeral=True,
    )
    interaction.response.send_message.assert_not_awaited()
  1. If BaseView lives in a different module than capy_discord.ui.view, update the import and both monkeypatch.setattr targets accordingly.
  2. If your test suite uses a different async test marker (e.g. pytest.mark.anyio), adjust the @pytest.mark.asyncio decorators to match your configuration.
  3. If BaseView.on_error has a different signature or additional required parameters, update the await view.on_error(...) calls to match that signature.

@shamikkarkhanis
shamikkarkhanis merged commit 895c336 into develop Feb 6, 2026
4 checks passed
@shamikkarkhanis
shamikkarkhanis deleted the feature/capr-30-create-global-error-handler branch February 6, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant